<html>
  <head>
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <meta name="theme-color" content="#008000"/>
   <title>minimap_ml</title>
   <link rel="manifest" href="minimap_ml_manifest.json">
   <script>
    if('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/minimap_ml_sw.js')
        .then(function() {
              console.log('Service Worker Registered');
        });
    }
   </script>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
  <h1>minimap_ml</h1>
  <script src="three.js"></script>
  <script>
    let container, scene, camera, renderer, session, controllerMeshes, rayMeshes, planeMesh, containerMesh;
    let intersectScene, intersectCamera, renderTarget, terrainMeshes;
    let mesher = null;

    const controllerGeometry = new THREE.BoxBufferGeometry(0.1, 0.2, 0.01);
    const controllerMaterial = new THREE.MeshPhongMaterial({
      color: 0xFF0000,
    });
    const _makeControllerMesh = () => {
      const mesh = new THREE.Mesh(controllerGeometry, controllerMaterial);
      mesh.position.set(i === 0 ? -0.1 : 0.1, 0, -0.1);
      mesh.quaternion.setFromUnitVectors(
        new THREE.Vector3(0, 0, -1),
        new THREE.Vector3(0, -1, -1)
      );
      mesh.updateMatrixWorld();
      mesh.matrixAutoUpdate = false;
      mesh.frustumCulled = false;
      return mesh;
    };
    const _getControllerIndex = inputSource => inputSource.handedness === 'left' ? 0 : 1;

    const localMatrix = new THREE.Matrix4();
    const localFloat32Array = new Float32Array(16);

    function init() {
      container = document.createElement('div');
      document.body.appendChild(container);

      scene = new THREE.Scene();
      scene.matrixAutoUpdate = false;
      // scene.background = new THREE.Color(0x3B3961);

      camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
      // camera.position.set(0, 1, 0);
      // camera.lookAt(new THREE.Vector3());
      scene.add(camera);

      const ambientLight = new THREE.AmbientLight(0x808080);
      scene.add(ambientLight);

      const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1);
      directionalLight.position.set(1, 1, 1);
      scene.add(directionalLight);

      controllerMeshes = Array(2);
      for (let i = 0; i < controllerMeshes.length; i++) {
        const controllerMesh = _makeControllerMesh();

        scene.add(controllerMesh);
        controllerMeshes[i] = controllerMesh;
      }

      const testMesh = new THREE.Mesh(
        new THREE.BoxBufferGeometry(1, 1, 1),
        new THREE.MeshPhongMaterial({
          color: 0xFFFF00,
        })
      );
      testMesh.position.z = -3;
      testMesh.frustumCulled = false;
      scene.add(testMesh);

      const _makeRayMesh = () => {
        const rayGeometry = new THREE.CylinderBufferGeometry(0.005, 0.005, 1, 3, 1)
          .applyMatrix(new THREE.Matrix4().makeTranslation(0, 1/2, 0))
          .applyMatrix(new THREE.Matrix4().makeRotationFromQuaternion(
            new THREE.Quaternion().setFromUnitVectors(
              new THREE.Vector3(0, 1, 0),
              new THREE.Vector3(0, 0, -1),
            )
          ));
        const rayMaterial = new THREE.MeshPhongMaterial({
          color: 0xFF0000,
          flatShading: true,
        });
        const rayMesh = new THREE.Mesh(rayGeometry, rayMaterial);
        rayMesh.matrixAutoUpdate = false;
        rayMesh.frustumCulled = false;
        return rayMesh;
      };
      /* rayMeshes = [
        _makeRayMesh(),
        _makeRayMesh(),
      ];
      rayMeshes.forEach(rayMesh => {
        scene.add(rayMesh);
      }); */

      const localVector = new THREE.Vector3();
      const localVector2 = new THREE.Vector3();
      const localVector3 = new THREE.Vector3();
      const localVector4 = new THREE.Vector3();
      const localQuaternion = new THREE.Quaternion();
      const localMatrix = new THREE.Matrix4();
      const oneVector = new THREE.Vector3(1, 1, 1);
      const upVector = new THREE.Vector3(0, 1, 0);
      const zeroQuaternion = new THREE.Quaternion();

      terrainMeshes = [];
      const terrainMaterial = new THREE.MeshPhongMaterial({
        color: 0x3333FF,
      });
      /* const terrainHighlightMaterial = new THREE.MeshPhongMaterial({
        color: 0x3333FF,
      }); */
      const _getTerrainMesh = meshId => {
        let terrainMesh = terrainMeshes.find(terrainMesh => terrainMesh.meshId === meshId);
        if (!terrainMesh) {
          terrainMesh = _makeTerrainMesh(meshId);
          terrainMeshes.push(terrainMesh);
          intersectScene.add(terrainMesh);
          // scene.add(terrainMesh);
        }
        return terrainMesh;
      };
      const fakeArrayBuffer = new ArrayBuffer(3 * 4);
      const fakeFloat32Array = new Float32Array(fakeArrayBuffer, 0, 3);
      const fakeUint16Array = new Uint16Array(fakeArrayBuffer, 0, 3);
      const _makeTerrainMesh = meshId => {
        const geometry = new THREE.BufferGeometry();
        const gl = renderer.getContext();
        const attributes = renderer.getAttributes();

        geometry.addAttribute('position', new THREE.BufferAttribute(fakeFloat32Array, 3));
        attributes.update(geometry.attributes.position, gl.ARRAY_BUFFER);
        geometry.addAttribute('normal', new THREE.BufferAttribute(fakeFloat32Array, 3));
        attributes.update(geometry.attributes.normal, gl.ARRAY_BUFFER);
        geometry.setIndex(new THREE.BufferAttribute(fakeUint16Array, 1));
        attributes.update(geometry.index, gl.ELEMENT_ARRAY_BUFFER);

        const material = terrainMaterial;

        const mesh = new THREE.Mesh(geometry, material);
        mesh.matrixAutoUpdate = false;
        mesh.frustumCulled = false;
        mesh.meshId = meshId;
        return mesh;
      };
      const _loadTerrainMesh = (terrainMesh, {transformMatrix, positionBuffer, positionCount, normalBuffer, normalCount, indexBuffer, count}) => {
        terrainMesh.matrix.fromArray(transformMatrix);
        terrainMesh.matrixWorldNeedsUpdate = true;

        const {geometry} = terrainMesh;
        const attributes = renderer.getAttributes();

        attributes.get(geometry.attributes.position).buffer = positionBuffer;
        geometry.attributes.position.count = positionCount / 3;

        attributes.get(geometry.attributes.normal).buffer = normalBuffer;
        geometry.attributes.normal.count = normalCount / 3;

        attributes.get(geometry.index).buffer = indexBuffer;
        geometry.index.count = count / 1;
      };
      const _removeTerrainMesh = terrainMesh => {
        intersectScene.remove(terrainMesh);
        // scene.remove(terrainMesh);
        terrainMesh.geometry.dispose();
        terrainMeshes.splice(terrainMeshes.indexOf(terrainMesh), 1);
      };
      const _onMesh = updates => {
        try {
          // console.log('got updates', updates.length);

          for (let i = 0; i < updates.length; i++) {
            const update = updates[i];
            const {id, type} = update;

            // console.log('got update', {id, type});

            if (type === 'new' || type === 'update') {
              const terrainMesh = _getTerrainMesh(id);
              terrainMesh.material = terrainMaterial;
              _loadTerrainMesh(terrainMesh, update);
            } else if (type === 'unchanged') {
              // const terrainMesh = terrainMeshes.find(terrainMesh => terrainMesh.meshId === id);
              // terrainMesh.material = terrainHighlightMaterial;
            } else {
              const terrainMeshIndex = terrainMeshes.findIndex(terrainMesh => terrainMesh.meshId === id);
              if (terrainMeshIndex !== -1) {
                const terrainMesh = terrainMeshes[terrainMeshIndex];
                _removeTerrainMesh(terrainMesh);
              }
            }
          }
        } catch(err) {
          console.warn(err.stack);
        }
      };

      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true,
      });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);

      if (window.browser && window.browser.magicleap) {
        mesher = window.browser.magicleap.RequestMeshing();
        mesher.onmesh = _onMesh;
      } else {
        const gl = renderer.getContext();

        const transformMatrix = localMatrix
          .fromArray(window.document.xrOffset.matrix)
          .getInverse(localMatrix)
          .toArray(localFloat32Array);

        const positionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        const positions = Float32Array.from([0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5, -0.5, -0.5]);
        gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
        const positionCount = positions.length;

        const normalBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
        const normals = Float32Array.from([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1]);
        gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);
        const normalCount = normals.length;

        const indexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        const indices = Uint16Array.from([0, 2, 1, 2, 3, 1, 4, 6, 5, 6, 7, 5, 8, 10, 9, 10, 11, 9, 12, 14, 13, 14, 15, 13, 16, 18, 17, 18, 19, 17, 20, 22, 21, 22, 23, 21]);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
        const count = indices.length;

        renderer.state.reset();

        const updates = [
          {
            id: 0,
            type: 'update',
            transformMatrix,
            positionBuffer,
            positionCount,
            normalBuffer,
            normalCount,
            indexBuffer,
            count,
          },
        ];
        setInterval(() => {
          _onMesh(updates);
        }, 100);
      }

      (() => {
        intersectScene = new THREE.Scene();
        intersectScene.matrixAutoUpdate = false;

        intersectCamera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
        intersectCamera.matrixAutoUpdate = false;
        intersectScene.add(intersectCamera);

        /* const skybox = new THREE.Mesh(
          new THREE.BoxBufferGeometry(3, 3, 3)
            .applyMatrix(new THREE.Matrix4().makeScale(-1, -1, -1)),
          new THREE.MeshPhongMaterial({
            color: 0x0000FF,
            flatShading: true,
          })
        );
        skybox.frustumCulled = false;
        intersectScene.add(skybox); */

        const ambientLight = new THREE.AmbientLight(0x808080);
        intersectScene.add(ambientLight);

        const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1);
        directionalLight.position.set(1, 1, 1);
        intersectScene.add(directionalLight);

        renderTarget = new THREE.WebGLRenderTarget(512, 512, {
          // XXX
        });

        planeMesh = new THREE.Mesh(
          new THREE.PlaneBufferGeometry(0.2, 0.2)
            .applyMatrix(new THREE.Matrix4().makeTranslation(0, 0.2/2, 0)),
          new THREE.MeshBasicMaterial({
            // color: 0x0000FF,
            map: renderTarget.texture,
          })
        );
        // planeMesh.position.z = -1;
        // planeMesh.updateMatrixWorld();
        planeMesh.matrixAutoUpdate = false;
        // planeMesh.updateMatrixWorld();
        planeMesh.matrixWorldNeedsUpdate = true;
        planeMesh.frustumCulled = false;
        scene.add(planeMesh);

        containerMesh = new THREE.Object3D();
        // containerMesh.position.t = 0.1;
        // containerMesh.scale.set(0.001, 0.001, 0.001);
        // containerMesh.updateMatrixWorld(true);
        containerMesh.visible = false;
        containerMesh.frustumCulled = false;
        scene.add(containerMesh);
      })();

      container.appendChild(renderer.domElement);

      renderer.setAnimationLoop(animate);
    }

    const localVector = new THREE.Vector3();
    const localQuaternion = new THREE.Quaternion();
    const localEuler = new THREE.Euler();
    const localVector2 = new THREE.Vector3();
    // const pixel = new Uint8Array(new ArrayBuffer(4));
    const lastPads = [false, false];
    let mode3d = false;
    function animate(time, frame) {
      if (renderer.vr.enabled) {
        for (let i = 0; i < controllerMeshes.length; i++) {
          controllerMeshes[i].visible = false;
        }

        const inputSources = session.getInputSources();
        for (let i = 0; i < inputSources.length; i++) {
          const inputSource = inputSources[i];
          const pose = frame.getInputPose(inputSource);

          const controllerIndex = _getControllerIndex(inputSource);
          const controllerMesh = controllerMeshes[controllerIndex];
          controllerMesh.matrix.fromArray(pose.targetRay.transformMatrix);
          // controllerMesh.updateMatrixWorld();
          controllerMesh.matrixWorldNeedsUpdate = true;
          // controllerMesh.matrixWorld.multiplyMatrices(controllerMesh.parent.matrixWorld, controllerMesh.matrix);
          // controllerMesh.matrixWorld.fromArray(pose.targetRay.transformMatrix);
          // controllerMesh.matrix.decompose(controllerMesh.position, controllerMesh.quaternion, controllerMesh.scale);
          // controllerMesh.updateMatrixWorld(true);
          controllerMesh.visible = true;

          // console.log('controller position', controllerMesh.position.toArray().join(','));

          if (i === 0) { // left
            intersectCamera.matrix.fromArray(pose.targetRay.transformMatrix);
            intersectCamera.matrix.decompose(intersectCamera.position, intersectCamera.quaternion, intersectCamera.scale);
            // console.log('position', intersectCamera.position.toArray().join(','));
            intersectCamera.updateMatrixWorld(true);

            if (planeMesh.visible) {
              planeMesh.matrix.fromArray(pose.targetRay.transformMatrix);
              planeMesh.matrix.decompose(planeMesh.position, planeMesh.quaternion, planeMesh.scale);
              planeMesh.updateMatrixWorld(true);
            } else {
              containerMesh.matrix.fromArray(pose.targetRay.transformMatrix);
              containerMesh.matrix.decompose(containerMesh.position, containerMesh.quaternion, containerMesh.scale);
              containerMesh.position.y += 0.1;
              localEuler.setFromQuaternion(containerMesh.quaternion, 'YXZ');
              localEuler.x = 0;
              localEuler.z = 0;
              containerMesh.quaternion.setFromEuler(localEuler);
              containerMesh.scale.set(0.05, 0.05, 0.05);
              containerMesh.matrix.compose(containerMesh.position, containerMesh.quaternion, containerMesh.scale);
              // containerMesh.updateMatrixWorld(true);
            }
          }
        }

        const gamepads = navigator.getGamepads();
        for (let i = 0; i < gamepads.length; i++) {
          const gamepad = gamepads[i];

          if (gamepad) {
            const controllerIndex = gamepad.hand === 'left' ? 0 : 1;

            const pad = gamepad.buttons[0].pressed;
            const lastPad = lastPads[controllerIndex];
            if (pad && !lastPad) {
              mode3d = !mode3d;

              console.log('change mode', mode3d);

              if (mode3d) {
                for (let i = 0; i < terrainMeshes.length; i++) {
                  containerMesh.add(terrainMeshes[i]);
                }
                containerMesh.updateMatrixWorld(true);

                containerMesh.visible = true;
                planeMesh.visible = false;
              } else {
                for (let i = 0; i < terrainMeshes.length; i++) {
                  intersectScene.add(terrainMeshes[i]);
                }
                intersectScene.updateMatrixWorld(true);

                planeMesh.visible = true;
                containerMesh.visible = false;
              }
            }
            lastPads[controllerIndex] = pad;
          }
        }
      }

      if (planeMesh.visible) {
        const oldVrEnabled = renderer.vr.enabled;
        renderer.vr.enabled = false;
        renderer.setRenderTarget(renderTarget);
        renderer.render(intersectScene, intersectCamera, renderTarget);
        // renderer.readRenderTargetPixels(renderTarget, 512/2, 512/2, 1, 1, pixel) 
        // console.log('pixel', pixel.join(','));
        renderer.setRenderTarget(null);
        renderer.vr.enabled = oldVrEnabled;
      }

      renderer.render(scene, renderer.vr.enabled ? renderer.vr.getCamera(camera) : camera);
    }

    init();

    (async () => {
      if (navigator.xr) {
        console.log('request session');
        session = await navigator.xr.requestSession({
          exclusive: true,
        }).catch(err => Promise.resolve(null));

        if (session) {
          // console.log('request first frame');
          session.requestAnimationFrame((timestamp, frame) => {
            renderer.vr.setSession(session, {
              frameOfReferenceType: 'stage',
            });

            const {views} = frame.getViewerPose();
            const viewport = session.baseLayer.getViewport(views[0]);
            const width = viewport.width;
            const height = viewport.height;

            renderer.setSize(width * 2, height);

            renderer.setAnimationLoop(null);

            renderer.vr.enabled = true;
            renderer.vr.setAnimationLoop(animate);

            console.log('running xr!');
          });
        }
      } else {
        renderer.vr.setAnimationLoop(animate);

        console.log('running 2d!');
      }
    })();
  </script>
  </body>
</html>
